<?php
/**
 * Parses and verifies the doc comments for classes.
 *
 * PHP version 5
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @author    Marc McIntyre <mmcintyre@squiz.net>
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
 * @version   CVS: $Id: ClassCommentSniff.php,v 1.18 2008/02/06 02:54:57 squiz Exp $
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */

require_once('FileCommentSniff.php');

if (class_exists('PHP_CodeSniffer_CommentParser_ClassCommentParser', true) === false) {
  $error = 'Class PHP_CodeSniffer_CommentParser_ClassCommentParser not found';
  throw new PHP_CodeSniffer_Exception($error);
}

if (class_exists('Fajr_Sniffs_Comment_FileCommentSniff', true) === false) {
  $error = 'Class Fajr_Sniffs_Comment_FileCommentSniff not found';
  throw new PHP_CodeSniffer_Exception($error);
}

/**
 * Parses and verifies the doc comments for classes.
 *
 * Verifies that :
 * <ul>
 *  <li>A doc comment exists.</li>
 *  <li>There is a blank newline after the short description.</li>
 *  <li>There is a blank newline between the long and short description.</li>
 *  <li>There is a blank newline between the long description and tags.</li>
 *  <li>Check the order of the tags.</li>
 *  <li>Check the indentation of each tag.</li>
 *  <li>Check required and optional tags and the format of their content.</li>
 * </ul>
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @author    Marc McIntyre <mmcintyre@squiz.net>
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
 * @version   Release: 1.1.0
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */
class Fajr_Sniffs_Comment_ClassCommentSniff extends Fajr_Sniffs_Comment_FileCommentSniff
{


  /**
   * Returns an array of tokens this test wants to listen for.
   *
   * @return array
   */
  public function register()
  {
    return array(
        T_CLASS,
        T_INTERFACE,
        );

  }//end register()


  /**
   * Processes this test, when one of its tokens is encountered.
   *
   * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
   * @param int                  $stackPtr  The position of the current token in the
   *                                        stack passed in $tokens.
   *
   * @return void
   */
  public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
  {
    $this->currentFile = $phpcsFile;

    $tokens = $phpcsFile->getTokens();
    $type   = strtolower($tokens[$stackPtr]['content']);
    $find   = array(
        T_ABSTRACT,
        T_WHITESPACE,
        T_FINAL,
        );

    // Extract the class comment docblock.
    $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);

    if ($commentEnd !== false && $tokens[$commentEnd]['code'] === T_COMMENT) {
      $phpcsFile->addError("You must use \"/**\" style comments for a $type comment", $stackPtr);
      return;
    } else if ($commentEnd === false || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT) {
      $phpcsFile->addError("Missing $type doc comment", $stackPtr);
      return;
    }

    $commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
    $commentNext  = $phpcsFile->findPrevious(T_WHITESPACE, ($commentEnd + 1), $stackPtr, false, $phpcsFile->eolChar);

    // Distinguish file and class comment.
    $prevClassToken = $phpcsFile->findPrevious(T_CLASS, ($stackPtr - 1));
    if ($prevClassToken === false) {
      // This is the first class token in this file, need extra checks.
      $prevNonComment = $phpcsFile->findPrevious(T_DOC_COMMENT, ($commentStart - 1), null, true);
      if ($prevNonComment !== false) {
        $prevComment = $phpcsFile->findPrevious(T_DOC_COMMENT, ($prevNonComment - 1));
        if ($prevComment === false) {
          // There is only 1 doc comment between open tag and class token.
          $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), $stackPtr, false, $phpcsFile->eolChar);
          if ($newlineToken !== false) {
            $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($newlineToken + 1), $stackPtr, false, $phpcsFile->eolChar);
            if ($newlineToken !== false) {
              // Blank line between the class and the doc block.
              // The doc block is most likely a file comment.
              $phpcsFile->addError("Missing $type doc comment", ($stackPtr + 1));
              return;
            }
          }//end if
        }//end if
      }//end if
    }//end if

    $comment = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));

    // Parse the class comment.docblock.
    try {
      $this->commentParser = new PHP_CodeSniffer_CommentParser_ClassCommentParser($comment, $phpcsFile);
      $this->commentParser->parse();
    } catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
      $line = ($e->getLineWithinComment() + $commentStart);
      $phpcsFile->addError($e->getMessage(), $line);
      return;
    }

    $comment = $this->commentParser->getComment();
    if (is_null($comment) === true) {
      $error = ucfirst($type).' doc comment is empty';
      $phpcsFile->addError($error, $commentStart);
      return;
    }

    // No extra newline before short description.
    $short        = $comment->getShortComment();
    $newlineCount = 0;
    $newlineSpan  = strspn($short, $phpcsFile->eolChar);
    if ($short !== '' && $newlineSpan > 0) {
      $line  = ($newlineSpan > 1) ? 'newlines' : 'newline';
      $error = "Extra $line found before $type comment short description";
      $phpcsFile->addError($error, ($commentStart + 1));
    }

    $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);

    // Exactly one blank line between short and long description.
    $long = $comment->getLongComment();
    if (empty($long) === false) {
      $between        = $comment->getWhiteSpaceBetween();
      $newlineBetween = substr_count($between, $phpcsFile->eolChar);
      if ($newlineBetween !== 2) {
        $error = "There must be exactly one blank line between descriptions in $type comments";
        $phpcsFile->addError($error, ($commentStart + $newlineCount + 1));
      }

      $newlineCount += $newlineBetween;
    }

    // Exactly one blank line before tags.
    $tags = $this->commentParser->getTagOrders();
    if (count($tags) > 1) {
      $newlineSpan = $comment->getNewlineAfter();
      if ($newlineSpan !== 2) {
        $error = "There must be exactly one blank line before the tags in $type comments";
        if ($long !== '') {
          $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
        }

        $phpcsFile->addError($error, ($commentStart + $newlineCount));
        $short = rtrim($short, $phpcsFile->eolChar.' ');
      }
    }

    // Check each tag.
    $this->processTags($commentStart, $commentEnd);

  }//end process()


  /**
   * Process the version tag.
   *
   * @param int $errorPos The line number where the error occurs.
   *
   * @return void
   */
  protected function processVersion($errorPos)
  {
    $version = $this->commentParser->getVersion();
    if ($version !== null) {
      $content = $version->getContent();
      $matches = array();
      if (empty($content) === true) {
        $error = 'Content missing for @version tag in doc comment';
        $this->currentFile->addError($error, $errorPos);
      } else if ((strstr($content, 'Release:') === false)) {
        $error = "Invalid version \"$content\" in doc comment; consider \"Release: <package_version>\" instead";
        $this->currentFile->addWarning($error, $errorPos);
      }
    }

  }//end processVersion()


}//end class

?>
